基于Java代码模型生成质量平台自动化用例方案与实践 | 得物技术
目录
一、背景&目的
二、聊聊自动化那些事
1. “低代码” 银弹 v.s 毒瘤
2. 浅谈自动化用例生成的几种方案
三、基于Java代码模型生成自动化用例实践&方案
1. 方案&设计
2. 注解生命周期
3. 平台核心组件
4. 基于Java注解 & “重设计,轻实现”设计理念驱动
5. 一站式生成测试用例服务
6. 可扩展&维护性
7. Auto Gen Auto 所有测试工作即代码
8. 自动化ROI
四、价值&收益
1. 一个订单税费自动化测试用例设计演示
2. 价值&收益
一
背景&目的
得物自动化从最开始由各域自建到质量平台统一搭建自动化测试基础服务中台发展过程,目前在质量平台的支撑下实现了全域自动化的统一管理和维护,进一步降低了自动化测试的成本同时也提高了自动化测试效率。在质量平台的支撑下如何进一步快速、高效实现自动化用例开发和维护提高ROI也是值得探索和创新的。我们都知道自动化用例的开发和维护成本一直是自动化测试领域老生常谈的话题,本次分享结合了低代码思想和Java代码模型快速的生成质量平台自动化测试用例方案与实践,主要是为了解决:
提升自动化用例开发效率
降低自动化用例维护成本
“重设计,轻实现”设计驱动
二
聊聊自动化那些事
“低代码”银弹 v.s 毒瘤
浅谈自动化用例生成的几种方案
流量录制&字节码
基于流量录制 (UI & http & dubbo)
基于字节码技术
平台化&智能化
设计驱动
统一平台载体
低代码&自动化生成技术
基于大模型和AI技术
L0,原始级
测试工程师还是在做测试用例设计、执行、回归、修复后再回归。没有专职的人写自动化的脚本。测试人员按需撰写脚本,遇到较大变更的时候还要检查脚本是否有效。
L1,辅助级
测试框架能帮助测试工程师完成一些枯燥乏味的工作,通过一些算法完善测试脚本并将测试结果发送给对应的工程师,由工程师来决策测试结果。
L2,部分自动化级
自动化测试的算法可以自我容错,不需要大量的维护工作,会按照测试用例去执行与识别,不会影响执行流程。然后它会把测试结果发送给测试工程师,由工程师决策测试。
L3,有条件的自动化级
测试工程师能建立自己的测试基线与准则,测试框架可以通过机器学习完成基线的建立,可以在无人干预的情况下完成测试,测试工程师只需要了解被测系统和数据规则即可,并自动的确定Bug。并且还可以收集并分析全部的测试用例,通过机器学习等相关技术,人工智能系统可以检测到变化中的异常, 并只将异常提交给人工进行验证。
L4,高度自动化级
系统能模拟类人的行为,进行并执行一些逻辑脚本或者业务脚本的撰写,达到一种完美的人机交互。它可以将测试结果定优先级,会根据严重程度发到测试管理系统,但不会对没有样本的做定义,还需要人类决策。
L5,全量自动化级
系统能够完成过程化的测试,了解产品的变更,知道产品的黄金流程,同时还会将问题完全反馈。测试工程师在这里仅仅做的是算法逻辑的维护、规则的维护。
三
基于Java代码模型生成自动化用例实践&方案
方案&设计
注解生命周期
@TestConfig:描述测试用例的基本信息 @TestData:测试数据准备,主要是编写对应sql来获取数据,也支持自定义方法 @BeforeAlls:测试用例固定参数,例如用户名、密码等固定参数 @Component:顾名思义组件的意思,通过Component进行组件编排 @UrlParameter:接口URL请求参数 @HeaderParameter:接口header参数 @ApiSchedule:http、https、dubbo接口信息 @Asserts:接口断言,支持接口返回、数据、MQ等断言
@TestConfig
@TestConfig
private static TestCaseParameter bases() {
TestCaseParameter testCaseParameter = new TestCaseParameter();
testCaseParameter.setDescribe("xxxxxxxxx"); // 测试用例描述
testCaseParameter.setPriority(PriorityConst.P1); // 测试用例级别
testCaseParameter.setLabel(LabelConst.BVT); // 测试用例类型
testCaseParameter.setModule(ModelConst.TEST); // 测试用例模块
testCaseParameter.setRemove(true);
return testCaseParameter;
}
@Parameters
@BeforeAlls
public static Map beforeAlls() {
Map<String, Object> beforeAlls = new LinkedHashMap<>();
beforeAlls.put("service", "xxxxxx");
beforeAlls.put("userName", "xxxxxx");
beforeAlls.put("scene", "USER_CENTER");
return beforeAlls;
}
@TestData
@TestData(
type = DataType.AFTER,
order = 0
)
public static Database testData01() {
Database database = new Database();
database.setDb(AppDbEnum.HUPU_DU.getName());
database.setSql("select userId from users where userName = '${userName}' and region = 'US';");
return database;
}
// 1.DataType.AFTER测试用例执行之后从数据库提取数据
// 2.DataType.BEFORE测试用例执行之后从数据库提取数据
// 3.order参数值和API绑定
@Component
@Component
private static List<String> component() {
List<String> componentList = new ArrayList<>();
componentList.add(ComponentEnum.USER_COOKIES.getName());
return componentList;
}
// 自动化平台开发好的组件
@UrlParameter
@UrlParameter(
order = 1
)
public static Map<String, Object> urlParameter01() {
Map<String, Object> urlParameter = new LinkedHashMap<>();
urlParameter.add("userId","xxxxx");
return urlParameter;
}
// 接口URL参数,order和@ApiSchedule API绑定
@HeaderParameter
@HeaderParameter(
order = 0
)
public static Map headerParameter01() {
Map<String, Object> headerParameter = new LinkedHashMap<>();
headerParameter.put("userId", "xxxxx");
headerParameter.put("Content-Type", "application/json");
return headerParameter;
}
// 接口header参数,order和@ApiSchedule API绑定
@ApiSchedule
@ApiSchedule(
order = 0
)
public static TestApiInfo apiSchedule00() {
TestApiInfo testApiInfo = new TestApiInfo();
testApiInfo.setDescribe("xxxxxxxxxx"); // api接口信息描述
testApiInfo.setApp("intl-bigger"); // api对应服务部署服务名
testApiInfo.setHost("xxxxxxxx");
testApiInfo.setUrl("/seller/confirm");
testApiInfo.setMothod(RequestMethodConst.POST);
testApiInfo.setBody("{\n"
+ " \"list\": [\n"
+ " {\n"
+ " \"skuId\": \"6040346982\",\n"
+ " \"codeInfo\": \"4057283582767\",\n"
+ " \"origin\": \"COMPETITION\",\n"
+ " \"codeOrigin\": \"nice\"\n"
+ " },\n"
+ " {\n"
+ " \"skuId\": \"6040346983\",\n"
+ " \"codeInfo\": \"4057283582767\",\n"
+ " \"origin\": \"COMPETITION\",\n"
+ " \"codeOrigin\": \"nice\"\n"
+ " }\n"
+ " ]\n"
+ "}");
List<String> component = new ArrayList<>();
component.add(ComponentEnum.USER_COOKIES.getName() + "_true");
component.add(ComponentEnum.AUTO_GLOBAL_LIST_ASSERTION.getName() + "_false");
Map<String, Object> bParameter = new LinkedHashMap<>();
bParameter.put("userName", "huxiaotian@shizhuang-inc.com");
bParameter.put("passWord", "du123456");
bParameter.put("time_pre", "uuid.uuid()_vbs");
testApiInfo.setBParameter(bParameter);
Map<String, Object> fParameter = new LinkedHashMap<>();
fParameter.put("userName", "huxiaotian@shizhuang-inc.com");
fParameter.put("passWord", "du123456");
fParameter.put("time_pre", "uuid.uuid()_vbs");
testApiInfo.setFParameter(fParameter);
testApiInfo.setComponent(component);
testApiInfo.setIsFront(true);
return testApiInfo;
}
@ApiSchedule(
order = 1
)
public static TestApiInfo apiSchedule01() {
TestApiInfo testApiInfo = new TestApiInfo();
testApiInfo.setDescribe("xxxxxxxxxx");
testApiInfo.setApp("intl-bigger");
testApiInfo.setHost("xxxxxxxx");
testApiInfo.setUrl("/seller/cancel");
testApiInfo.setMothod(RequestMethodConst.POST);
testApiInfo.setBody("{\n"
+ " \"list\": [\n"
+ " {\n"
+ " \"skuId\": \"6040346982\",\n"
+ " \"codeInfo\": \"4057283582767\",\n"
+ " \"origin\": \"COMPETITION\",\n"
+ " \"codeOrigin\": \"nice\"\n"
+ " },\n"
+ " {\n"
+ " \"skuId\": \"6040346983\",\n"
+ " \"codeInfo\": \"4057283582767\",\n"
+ " \"origin\": \"COMPETITION\",\n"
+ " \"codeOrigin\": \"nice\"\n"
+ " }\n"
+ " ]\n"
+ "}");
Map<String, Object> bParameter = new LinkedHashMap<>();
bParameter.put("time_sleep", "time.sleep(6)_vbs");
testApiInfo.setBParameter(bParameter);
return testApiInfo;
}
// 用例执行按照order值从小到大
@Extract
@Extract(order = 0)
private static List<String> extract() {
List<String> extract = new ArrayList<>();
extract.add("staus=$..staus");
return extract;
}
// 提取接口返回报文中的字段信息,编写规则完全遵循jsonpath语法规则:$..date..list[0]..order
@Asserts
@Asserts(
type = AssertType.RESPONSE,
order = 0
)
public static List<String> response00() {
List<String> rule = new ArrayList<>();
rule.add("$..amountInfo!=null");
rule.add("$..date..list[0]..freightAmount=20");
rule.add("$..saleTaxAmount>100");
return rule;
}
// AssertType.RESPONSE:接口返回报文断言,$..date..list[0]..freightAmount是根据jsonpath规则提取接口返回报文的字段信息来断言;
// 支持断言操作符:=、!=、>=、<=、>、<、<>
@Asserts(
type = AssertType.SQL,
order = 0
)
private static DbAssert asserts() {
DbAssert dbAssert = new DbAssert();
String db = OverseaDatabaseConst.DW_INTL_ORDER;
String table = "intl_trade_order_item";
String sql = "SELECT order_item_no,saleTaxAmount,freightAmount FROM `intl_trade_order_item` where order_item_no = '${order_item_no}';";
Map<String, String> rules = new LinkedHashMap<>();
rules.put("sql.$..order_item_no=${order_item_no}");
rules.put("sql.$..saleTaxAmount>=200");
rules.put("sql.$..order_item_no=500");
dbAssert.setDb(db);
dbAssert.setTable(table);
dbAssert.setSql(sql);
dbAssert.setRules(rules);
return dbAssert;
}
// AssertType.SQL:是针对接口实现SQL断言
// 支持断言操作符:=、!=、>=、<=、>、<、<>
平台核心组件
接口返回断言
$..data[0]..inboundApplyNo!=null
$..data[0]..status=1
$..data[0]..status<>1,3,4,5
$..status=request.$..status
$..name[*]=4
// 支持断言操作符:=、!=、>=、<=、>、<、<>
接口数据库断言
sql.$..biz_no=null
sql.$..biz_no!=null
sql.$..status=request.$..status
sql.$..biz_no=response.$..biz_no
sql.$..feature..status=5
// 支持断言操作符:=、!=、>=、<=、>、<、<>
基于Java注解&“重设计,轻实现”设计理念驱动
@RunWith(GenerateCode.class)
@Property(
source = RegionEnum.GUS_APP,
packages = "com.platform.autotestcase.testcases.sg.bvt.order",
describe = "xxxxxxxxxxx",
module = ModuleEnum.TEST,
beforeAll = {
@Args(key = "reverse_no", value = "xxxxxxx", clazz = Demo.class, method = "parme"),
@Args(key = "time_pre", vbs = "uuid.uuid()"),
},
mysql = {
@Database(db = AppDbEnum.DW_INTL_DEPOSIT, sql = "update outbound_apply set status = 5,signed_time=null where reverse_no = '${userName}';"),
@Database(db = AppDbEnum.DW_INTL_DEPOSIT, sql = "select order_no outbound_apply where status = 5 and reverse_no = '${userName}';"),
},
component = {
ComponentEnum.USER_COOKIES,
},
testData = {
@Source(type = DataType.AFTER,
order = 0,
db = AppDbEnum.HUPU_DU,
sql = "select userId from users where userName = '${userName}' and region = 'US';"),
@Source(type = DataType.BEFORE,
order = 0,
db = AppDbEnum.DW_INTL_ORDER,
sql = "select order_item_no from intl_trade_order_item where buyer_id = '${userId}' and order_item_status >= 2000 and order_item_status <= 4000 and biz_id = 'DEWU_NORMAL_EXPORT_2C' and package_info like '%SFGJIECS%' order by modify_time desc limit 1;"),
},
extract = {
@Filter(order = 0, key = "amountInfo", path = "$..amountInfo..minUnitVal"),
@Filter(order = 0, key = "freightAmount", path = "$..freightAmount..minUnitVal"),
@Filter(order = 0, key = "saleTaxAmount", path = "$..saleTaxAmount..minUnitVal"),
@Filter(order = 0, key = "paymentAmount", path = "$..paymentAmount..minUnitVal"),
},
rspAssert = {
@Regular(order = 0, path = "$..amountInfo!=null"),
@Regular(order = 0, path = "$..freightAmount!=null"),
@Regular(order = 0, path = "$..saleTaxAmount!=null"),
},
dbAssert = {
@Source(db = AppDbEnum.DW_INTL_ORDER,
order = 0,
sql = "select reverse_type,reverse_status,reason_code from intl_reverse_order where order_item_no = '${orderItem}' and is_del = 0;",
verify = {
@Args(path = "sql.$..reverse_status=200")
}
),
@Source(db = AppDbEnum.DW_INTL_ORDER,
order = 1,
sql = "select reverse_status,reason_code from intl_reverse_order where order_item_no = '${orderItem}' and is_del = 0;",
verify = {
@Args(path = "sql.$..reason_code=${reasonCode}"),
@Args(path = "sql.$..reverse_status!=200")
}
)
}
)
public class DemoTemplateTest {
@Http(order = 0, app = AppEnum.INTL_BIGGER, describe = "api描述")
@Assertion(
asserts = {
@Path(rule = "${orderItemNo}=34"),
@Path(rule = "${orderItemNo}=34"),
})
@Addition(
bParameter = {
@Args(key = "userName", value = "xxxxxx"),
@Args(key = "passWord", value = "xxxxxx"),
@Args(key = "time_pre", vbs = "uuid.uuid()"),
},
fParameter = {
@Args(key = "userName", value = "xxxxxxx"),
@Args(key = "passWord", value = "xxxxxxx"),
@Args(key = "time_pre", vbs = "uuid.uuid()"),
},
components = {
@Storage(component = ComponentEnum.USER_COOKIES, isFront = true),
@Storage(component = ComponentEnum.AUTO_GLOBAL_LIST_ASSERTION),
})
public static String curl =
"curl --location --globoff '{{myVariable}}:8888/deposit/enterprise-stock-apply/add' \\\n" +
"--header 'x-infr-flowtype: {{flowtype}}' \\\n" +
"--header 'Content-Type: application/json' \\\n" +
"--data '{\n" +
" \"globalSkuId\": 12000002083,\n" +
" \"applyQty\": 1,\n" +
" \"sellerId\": 10002366,\n" +
" \"timeZone\": \"Asia/Shanghai\",\n" +
" \"language\": \"en\"\n" +
"}'";
@Http(order = 1, app = AppEnum.INTL_BIGGER, describe = "api描述")
@Addition(
bParameter = {
@Args(key = "time_sleep", vbs = "time.sleep(6)"),
}
)
public static String cur1 =
"curl --location 'http://127.0.0.1:8888/tempCtrl/seller/cancel' \\\n" +
"--header 'userId: 45817899' \\\n" +
"--header 'Content-Type: application/json' \\\n" +
"--data '{\n" +
" \"list\": [\n" +
" {\n" +
" \"skuId\": \"6040346982\",\n" +
" \"codeInfo\": \"4057283582767\",\n" +
" \"origin\": \"COMPETITION\",\n" +
" \"codeOrigin\": \"nice\"\n" +
" },\n" +
" {\n" +
" \"skuId\": \"6040346983\",\n" +
" \"codeInfo\": \"4057283582767\",\n" +
" \"origin\": \"COMPETITION\",\n" +
" \"codeOrigin\": \"nice\"\n" +
" }\n" +
" ]\n" +
"}'";
}
一站式生成测试用例服务
可扩展&维护性
Auto Gen Auto 所有测试工作即代码
自动化ROI
t:单次运行时间
n:自动化测试运行次数
d:开发成本
m:维护成本
四
价值&收益
一个订单税费自动化测试用例设计演示
URL:/api/v1/h5/bigger/intl/bigger/buyer-order/confirm?noSign=true
Method: POST
Header:
Content-Type:application/jsonCurrencycode:USDDeviceid:5df7fa6f-e075-4f6f-96ea-c9b7d7514859Devicetype:h5Region:US
Body:
{
sizeKey:""
inventoryNo:"SN1021464791"
spuId:"30740440"
skuId:"602916395"
}
接口断言:
1.接口返回字段:minUnitVal字段值为不为0;
2.数据库断言:查询条件是接口返回requestId;
表1: oversea_sales_tax:新增1条税费记录;
表2: intl_trade_order_item:sale_tax_amount字段值大于“0”,status值为1000。
价值&收益
提升自动化用例开发的效率(80%+) 降低自动化维护成本&代码复用(80%+) 自动化脚本调试成本几乎是零 团队一致性:代码的可读性&团队共享
往期回顾
文 / Evan.hu
关注得物技术,每周一、三、五更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
“
扫码添加小助手微信
如有任何疑问,或想要了解更多技术资讯,请添加小助手微信: